/*
 * rabbit.c
 * Mike Zou <zxhmike@gmail.com>
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

#include <linux/fs.h>		/* file_operations */
#include <linux/kernel.h>	/* printk() */
#include <linux/slab.h>		/* kmalloc */
#include <linux/errno.h>
#include <linux/proc_fs.h>	/* the /proc file system */
#include <asm/uaccess.h>	/* copt_to/from_user */

#include "rabbit.h"

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Mike Zou");

static int rbt_major = 0;
static int rbt_minor = RBT_MINOR;
static int rbt_nr_devs = RBT_NR_DEVS;

static struct rbt_dev *rbt_devices;

static void rbt_clear(struct rbt_dev *dev)
{
	struct rbt_data_list *ptr, *next;
	dev->size = 0;
	if (!dev->data)
		return;
	for (ptr = (struct rbt_data_list*)dev->data; ptr; ptr = next) {
		if (ptr->data)
			kfree(ptr->data);
		next = ptr->next;
		kfree(ptr);
	}
	dev->data = NULL;
}

static struct rbt_data_list *rbt_new_list(struct rbt_data_list *prev)
{
	struct rbt_data_list *ptr;
	ptr = kmalloc(sizeof(struct rbt_data_list), GFP_KERNEL);
	if (!ptr)
		return NULL;
	memset(ptr, 0, sizeof(struct rbt_data_list));

	ptr->data = kmalloc(RBT_QUANTUM_SIZE * sizeof(char), GFP_KERNEL);
	if (!ptr->data) {
		kfree(ptr);
		return NULL;
	}
	memset(ptr->data, 0, RBT_QUANTUM_SIZE * sizeof(char));
	
	ptr->next = prev->next;
	prev->next = ptr;

	return ptr;	
}

/*
 * function rbt_follow
 * follow up the rbt list to the right position
 */
static struct rbt_data_list *rbt_follow(struct rbt_dev *dev, loff_t pos)
{
	int i, quantum_no;
	struct rbt_data_list *ptr = (struct rbt_data_list*)dev->data;
	quantum_no = (long)pos / RBT_QUANTUM_SIZE;

	/* if the first data_list does not exist, create it */
	if (!ptr) {
		ptr = kmalloc(sizeof(struct rbt_data_list), GFP_KERNEL);
		if (!ptr)
			return NULL;
		memset(ptr, 0, sizeof(struct rbt_data_list));
		ptr->data = kmalloc(RBT_QUANTUM_SIZE * sizeof(char), GFP_KERNEL);
		if (!ptr->data) {
			kfree(ptr);
			return NULL;
		}
		memset(ptr->data, 0, RBT_QUANTUM_SIZE * sizeof(char));
		dev->data = ptr;
		ptr->next = NULL;
	}

	i = quantum_no;
	while(i--) {
		if (!ptr->next) {
			ptr->next = rbt_new_list(ptr);
			if (!ptr->next)
				return NULL;
		}
		ptr = ptr->next;
	}
	return ptr;
}

int rbt_open(struct inode *inode, struct file *filp)
{
	struct rbt_dev *dev;
	dev = container_of(inode->i_cdev, struct rbt_dev, cdev);
	/* put the dev structure in the file pointer */
	filp->private_data = dev;

	/* if in write-only mode, clear the device buf */
	if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
		rbt_clear(dev);
	return 0;
}

int rbt_release(struct inode *inode, struct file *filp)
{
	return 0;
}

ssize_t rbt_write(struct file *filp, const char __user *buf,
	size_t count, loff_t *f_pos)
{
	struct rbt_dev *dev = (struct rbt_dev*)filp->private_data;
	struct rbt_data_list *list_ptr;
	int quantum_pos;
	unsigned long result;

	list_ptr = rbt_follow(dev, *f_pos);
	if (!list_ptr)
		return -EFAULT;
	
	quantum_pos = (int)((long)*f_pos % RBT_QUANTUM_SIZE);
	if (count + quantum_pos > RBT_QUANTUM_SIZE)
		count = RBT_QUANTUM_SIZE - quantum_pos;
	result = copy_from_user(list_ptr->data + quantum_pos, buf, count);
	if (result < 0)
		return result;
	dev->size += count;
	*f_pos += count;

	DEBUG("%d byte(s) written", count);
	return count;
}

ssize_t rbt_read(struct file *filp, char __user *buf,
	size_t count, loff_t *f_pos)
{
	struct rbt_dev *dev = (struct rbt_dev*)filp->private_data;
	struct rbt_data_list *list_ptr;
	int quantum_pos;
	unsigned long result;

	if (*f_pos > dev->size)
		return 0;
	
	list_ptr = rbt_follow(dev, *f_pos);
	if (!list_ptr)
		return -EFAULT;
	
	quantum_pos = (int)((long)*f_pos % RBT_QUANTUM_SIZE);
	if (count + quantum_pos > RBT_QUANTUM_SIZE)
		count = RBT_QUANTUM_SIZE - quantum_pos;
	if (count + *f_pos > dev->size)
		count = dev->size - *f_pos;
	result = copy_to_user(buf, list_ptr->data + quantum_pos, count);
	if (result < 0)
		return result;
	
	*f_pos += count;
	
	DEBUG("%d byte(s) read", count);
	return count;
}

struct file_operations rbt_fops = {
	.owner =	THIS_MODULE,
	.read =		rbt_read,
	.write = 	rbt_write,
	.open =		rbt_open,
	.release =	rbt_release,
};

int rbt_read_proc(char *buf, char **start, off_t offset,
	int count, int *eof, void *data)
{
	int dev_i, len = 0;
	struct rbt_dev *dev;
	int limit = count - 80; /* set the limit check */

	dev = &rbt_devices[0];
	for (dev_i = 0; dev_i < rbt_nr_devs; dev_i++) {
		len += sprintf(buf + len, "rabbit dev[%d], size = %d\n",
			dev_i, dev->size);
		dev++;
		if (len > limit)
			return len;
	}
	return len;
}

static void rbt_setup_cdev(struct rbt_dev *dev, int index)
{
	int result;
	int dev_no = MKDEV(rbt_major, rbt_minor + index);

	cdev_init(&dev->cdev, &rbt_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &rbt_fops;
	result = cdev_add (&dev->cdev, dev_no, 1);

	if (result)
		printk(KERN_WARNING "ERR %d adding rabbit%d",
			result, index);
}

static int __init rbt_init(void)
{
	int result, i;
	dev_t dev = 0;

	/* get a dynamic major version and register the device */
	result = alloc_chrdev_region(&dev, RBT_MINOR, 
		rbt_nr_devs, "rabbit");
	rbt_major = MAJOR(dev);
	if (result < 0) {
		printk(KERN_WARNING "rabbit: can't get major %d", rbt_major);
	}

	/* allocate the devices */
	rbt_devices = kmalloc(rbt_nr_devs * sizeof(struct rbt_dev), GFP_KERNEL);
	if (!rbt_devices)
		return -ENOMEM;
	memset(rbt_devices, 0, rbt_nr_devs * sizeof(struct rbt_dev));

	/* initialize each dev */
	for (i = 0; i < rbt_nr_devs; i++) {
		rbt_devices[i].size = 0;
		rbt_devices[i].data = NULL;
		rbt_setup_cdev(&rbt_devices[i], i);
	}

	/* register proc entry */
	create_proc_read_entry("rabbit", 0, NULL, rbt_read_proc, NULL);

	printk(KERN_ALERT "Module rabbit initialized");
	return 0;
}

static void __exit rbt_exit(void)
{
	int i;
	/* deallocate the momories */
	for (i = 0; i < rbt_nr_devs; i++) {
		rbt_clear(&rbt_devices[i]);
	}

	/* delete proc entry */
	remove_proc_entry("rabbit", NULL);

	/* unregister device */
	unregister_chrdev_region(MKDEV(rbt_major, rbt_minor), rbt_nr_devs);
	printk(KERN_ALERT "Module rabbit removed");
}

module_init(rbt_init);
module_exit(rbt_exit);
